﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace Shen.NET
{
    public interface IFunction
    {
        Object Apply(Object[] args, bool runRedirects);
        int Arity { get; }
    }

    public class Builtin : IFunction
    {
        public static Builtin Create(Func<Object> func)
        {
            return new Builtin(args => func(), 0);
        }

        public static Builtin Create<TA>(Func<TA, Object> func)
        {
            return new Builtin(args => func(args[0].As<TA>()), 1);
        }
        
        public static Builtin Create<TA, TB>(Func<TA, TB, Object> func)
        {
            return new Builtin(args => func(args[0].As<TA>(), args[1].As<TB>()), 2);
        }
        
        public static Builtin Create<TA, TB, TC>(Func<TA, TB, TC, Object> func)
        {
            return new Builtin(args => func(args[0].As<TA>(), args[1].As<TB>(), args[2].As<TC>()), 3);
        }

        private Builtin(Func<Object[], Object> func, int arity)
        {
            _func = func;
            Arity = arity;
        }

        private readonly Func<Object[], Object> _func;
        
        public Object Apply(Object[] args, bool runRedirects)
        {
            if (args.Length > Arity)
                throw new ApplicationException(String.Format("Too many arguments, expected {0}, {1} provided", Arity, args.Length));

            if (args.Length < Arity)
                return new Builtin(args2 => _func(Concat(args, args2)), Arity - args.Length);

            return _func(args);
        }

        public int Arity { get; private set; }

        public override string ToString()
        {
            return "Function/" + Arity;
        }

        private static Object[] Concat(Object[] a1, Object[] a2)
        {
            var z = new Object[a1.Length + a2.Length];
            a1.CopyTo(z, 0);
            a2.CopyTo(z, a1.Length);
            return z;
        }
    }

    public class Defun : IFunction
    {
        public Defun(DefunExpr expr, Context context) : this(expr.Params, expr.Body, context)
        {
            DefunExpr = expr;
        }

        public Defun(List<SymbolExpr> paramz, Expr body, Context context)
        {
            Body = body;
            Params = paramz;
            Context = context;
            Arity = paramz.Count;
        }

        public DefunExpr DefunExpr { get; private set; }
        public List<SymbolExpr> Params { get; private set; }
        public Expr Body { get; private set; }
        public Context Context { get; private set; }

        public Object Apply(Object[] args, bool runRedirects)
        {
            if (args.Length > Arity)
                throw new ApplicationException(String.Format("Too many arguments, expected {0}, {1} provided", Arity, args.Length));

            if (args.Length < Arity)
                return new Defun(Params.Skip(args.Length).ToList(), Body, Context.With(Params.Take(args.Length).Select(x => x.Id).ToArray(), args));

            return Body.Eval(Context.With(Params.Select(x => x.Id).ToArray(), args), runRedirects);
        }

        public int Arity { get; private set; }

        public override string ToString()
        {
            return "Function/" + Arity;
        }
    }
}
